package com.simpou.commons.utils.compiler;

import com.simpou.commons.utils.reflection.Casts;
import com.simpou.commons.utils.validation.Assertions;

import java.security.AccessController;
import java.security.PrivilegedAction;

import java.util.Arrays;

import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;


/**
 * Utilitário para compilação de código fonte Java em tempo de execução.
 *
 * @author Jonas Pereira
 * @since 2011-08-18
 * @version 2012-07-13
 */
public class RuntimeCompiler {
    /**
     * Compilador padrão do contexto atual de execução.
     */
    private static final JavaCompiler COMPILER;

    /**
     * ClassLoader padrão do contexto atual de execução.
     */
    private static final CompilerClassLoader CLASS_LOADER;

    static {
        CLASS_LOADER = getClassLoader(RuntimeCompiler.class.getClassLoader());
        COMPILER = ToolProvider.getSystemJavaCompiler();
    }

    private static CompilerClassLoader getClassLoader(final ClassLoader loader){
        PrivilegedAction<CompilerClassLoader> privilegedAction = new PrivilegedAction<CompilerClassLoader>() {
            @Override
            public CompilerClassLoader run() {
                return new CompilerClassLoader(loader);
            }
        };

        //Classloaders should only be created inside doPrivileged block
        CompilerClassLoader classLoader = AccessController.doPrivileged(privilegedAction);
        return  classLoader;
    }

    /**
     * <p>compile.</p>
     *
     * @param <T> Tipo da super classe.
     * @param superClass Super classe. A classe a ser gerada deve ser subclasse desta.
     * @param packageName Pacote.
     * @param className Nome da classe.
     * @param javaSource Código fonte a ser compilado.
     * @return Classe compilada.
     * @throws CompilerException Erros de compilação se houverem.
     */
    public static <T> Class<T> compile(final Class<T> superClass,
        final String packageName, final String className,
        final String javaSource) throws CompilerException {
        return compile(superClass, packageName, className, javaSource, null, null);
    }

    /**
     * <p>compile.</p>
     *
     * @param <T> Tipo da super classe.
     * @param superClass Super classe. A classe a ser gerada deve ser subclasse desta.
     * @param packageName Pacote.
     * @param className Nome da classe.
     * @param javaSource Código fonte a ser compilado.
     * @param compilationOptions Opções de compilação. Útil, por exemplo, para informar o CLASSPATH de compilação.
     * @return Classe compilada.
     * @throws CompilerException Erros de compilação se houverem.
     */
    public static <T> Class<T> compile(final Class<T> superClass,
        final String packageName, final String className,
        final String javaSource, final Iterable<String> compilationOptions, final ClassLoader classLoader)
        throws CompilerException {

        final CompilerClassLoader compilerClassLoader;
        if(classLoader == null){
            compilerClassLoader = CLASS_LOADER;
        }else{
            compilerClassLoader = getClassLoader(classLoader);
        }

        Assertions.notNull(COMPILER, "Compiler not found.");
        Assertions.notNull(superClass);
        Assertions.notEmpty(className);
        Assertions.notEmpty(javaSource);

        final String qualifiedClassName = getQualifiedClassName(packageName,
                className);
        final CompilerJavaFileObject sourceObj = new CompilerJavaFileObject(className,
                javaSource);
        final CompilerJavaFileObject compiledObj = new CompilerJavaFileObject(qualifiedClassName);
        final DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
        final StandardJavaFileManager standardFileManager = COMPILER.getStandardFileManager(diagnostics,
                null, null);
        final CompilerFileManager fileManager = new CompilerFileManager(standardFileManager,
                compilerClassLoader, compiledObj);

        final CompilationTask task = COMPILER.getTask(null, fileManager,
                diagnostics, compilationOptions, null, Arrays.asList(sourceObj));
        final boolean resultOk = task.call();

        if (!resultOk) {
            throw new CompilerException("Compilation fail.", diagnostics);
        }

        Class<?> loadClass;

        try {
            loadClass = compilerClassLoader.loadClass(qualifiedClassName);
        } catch (Exception ex) {
            throw new CompilerException("Compilation ok but fail on load class.",
                ex, diagnostics);
        }

        return Casts.rawClassCast(superClass, loadClass);
    }

    /**
     * @param className Nome da classe.
     * @param packageName Pacote da classe.
     * @return Nome canônico.
     */
    private static String getQualifiedClassName(final String packageName,
        final String className) {
        if ((packageName == null) || packageName.trim().isEmpty()) {
            return className;
        } else {
            return packageName + "." + className;
        }
    }
}
